// Copyright (c) 2009 Alistair Jones
// This program is made available under the terms of the MIT License.
package org.hypirinha.html.output;

import org.hypirinha.html.Content;
import org.hypirinha.html.attributes.AnyAttribute;
import org.hypirinha.html.attributes.Attribute;
import org.hypirinha.html.output.features.BasicEscaper;
import org.hypirinha.html.output.features.Doctype;
import org.hypirinha.html.output.features.Escaper;
import org.hypirinha.html.output.features.Namespace;
import org.hypirinha.html.output.features.XmlDeclaration;
import org.hypirinha.html.output.features.Encoding;
import static org.hypirinha.html.output.features.TypeFilter.filter;

import java.io.PrintWriter;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.StringWriter;
import java.util.Stack;

public class PrintAdapter {

    public interface Feature {};

    private Encoding encoding;
    private XmlDeclaration xmlDeclaration;
    private Doctype doctype;
    private Namespace namespace;
    private Escaper escaper;

    public PrintAdapter(Feature... features) {
        this(0,
            filter(features, Encoding.class, Encoding.UTF_8),
            filter(features, XmlDeclaration.class, XmlDeclaration.omit),
            filter(features, Doctype.class, Doctype.none),
            filter(features, Namespace.class, Namespace.unspecified),
            filter(features, Escaper.class, new BasicEscaper())
        );
    }

    private PrintAdapter(int workaroundAmbiguousConstructors, Encoding encoding, XmlDeclaration xmlDeclaration, Doctype doctype, Namespace namespace, Escaper escaper) {
        workaroundAmbiguousConstructors++;
        this.encoding = encoding;
        this.xmlDeclaration = xmlDeclaration;
        this.doctype = doctype;
        this.namespace = namespace;
        this.escaper = escaper;
    }

    public String asString(Content content) {
        StringWriter stringWriter = new StringWriter();
        print(content, stringWriter);
        return stringWriter.toString();
    }

    public void print(Content content, OutputStream outputStream) {
        print(content, new OutputStreamWriter(outputStream, encoding.getCharset()));
    }

    public void print(Content content, Writer writer) {
        PrintWriter printWriter = new PrintWriter(writer);
        xmlDeclaration.print(printWriter, encoding);
        doctype.print(printWriter);
        content.visit(new NamespaceDecorator(namespace, new PrintVisitor(printWriter, escaper)));
        printWriter.flush();
    }

    private static class PrintVisitor implements Visitor {

        private final PrintWriter printWriter;
        private final Escaper escaper;

        private String lineStart = "\n";
        private Stack<ElementContext> stack = new Stack<ElementContext>();

        public PrintVisitor(PrintWriter printWriter, Escaper escaper) {
            this.printWriter = printWriter;
            this.escaper = escaper;
            stack.push(new ElementContext("") {
                void beforeChild() {
                }
            });
        }

        public void startTag(String tagName, AnyAttribute... attributes) {
            stack.peek().beforeChild();
            stack.push(new ElementContext(tagName)).startTag(attributes);
            increaseIndent();
        }

        public void text(String text) {
            stack.peek().text(text);
        }

        public void endTag() {
            decreaseIndent();
            stack.pop().endTag();
        }

        private void increaseIndent() {
            lineStart += "  ";
        }

        private void decreaseIndent() {
            lineStart = lineStart.substring(0, lineStart.length() - 2);
        }

        private class ElementContext {
            private String tagName;
            private boolean hasChildren = false;
            private boolean firstElementWasText = false;

            ElementContext(String tagName) {
                this.tagName = tagName;
            }

            void startNewLineIfAppropriate() {
                if (hasChildren && !firstElementWasText) {
                    printWriter.print(lineStart);
                }
            }

            void startTag(AnyAttribute[] attributes) {
                printWriter.print("<" + tagName);
                for (AnyAttribute attribute : attributes) {
                    printWriter.print(" " + attribute.getName() + "=\"" + escaper.escapeAttributeValue(attribute.getValue()) + "\"");
                }
                printWriter.print(">");
            }

            void text(String text) {
                printWriter.print(escaper.escapeContent(text));
                if (!hasChildren) firstElementWasText = true;
            }

            void endTag() {
                startNewLineIfAppropriate();
                printWriter.print("</" + tagName + ">");
            }

            void beforeChild() {
                hasChildren = true;
                startNewLineIfAppropriate();
            }
        }
    }

    private class NamespaceDecorator implements Visitor {
        private Namespace namespace;
        private Visitor visitor;

        public NamespaceDecorator(Namespace namespace, Visitor visitor) {
            this.namespace = namespace;
            this.visitor = visitor;
        }

        public void startTag(String tagName, AnyAttribute... attributes) {
            if (namespace.isSpecified()) {
                AnyAttribute[] decorated = new AnyAttribute[attributes.length + 1];
                decorated[0] = new Attribute("xmlns", namespace.getUri());
                System.arraycopy(attributes, 0, decorated, 1, attributes.length);
                visitor.startTag(tagName, decorated);
                namespace = Namespace.unspecified;
            } else {
                visitor.startTag(tagName, attributes);
            }
        }

        public void text(String text) {
            visitor.text(text);
        }

        public void endTag() {
            visitor.endTag();
        }
    }
}
